Skip to content

Avoid tiny numpy arrays in numba: optimize intersections.py (and refactor: "un-scalarize") - #1727

Merged
Sevans711 merged 12 commits into
mainfrom
sevans/tinynumpynumba-optimize-intersections.py
Sep 9, 2026
Merged

Avoid tiny numpy arrays in numba: optimize intersections.py (and refactor: "un-scalarize")#1727
Sevans711 merged 12 commits into
mainfrom
sevans/tinynumpynumba-optimize-intersections.py

Conversation

@Sevans711

@Sevans711 Sevans711 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1726 (sub-issue of #1648)

Overview

Optimizes numba routines in uxarray/grid/intersections.py to avoid constructing many tiny numpy arrays inside numba routines, as discussed in #1648. ASV benchmarks seem to show roughly 2x speedup of intersections algorithms, and no performance degradations. Hard to know for sure how gca_gca intersection performance has been affected because those benchmarks are failing on main right now (see discussion on #1688 for details), but the results here at least seem to be the right order of magnitude (roughly 1 μs; latest successes of these benchmarks on main were roughly 1 to 1.5μs).

Additionally, refactors the intersections.py routines to restore prior behavior (from before #1688 merged) of using 3-vector inputs and outputs where possible. To maintain performance improvements, just use tuples instead of allocating tiny numpy arrays. The discussion in PR 1688 clarified the intent was to restore to the pre-scalarized function interfaces where possible without sacrificing performance.

A few changes are within scope of the original issue but the reasoning is not necessarily obvious directly from inspecting the code diff; clarifying here:

  • Previously, gca_gca_intersection output (len(result) == number of intersections) did not match its docstring, which promised shape (2,3) result but filling unused rows with NaNs. This PR updates the implementation to fill unused rows with NaNs, and always return a tuple of 2 length-3 tuples, like ((x1,y1,z1),(x2,y2,z2)), filling with NaNs to represent nonexistent intersection points. (The consistent shape is necessary for the performance optimization here. Numba complains that it "Can't unify return type" if the output length depends on the number of intersections.) The test suite has been updated accordingly, to check for nans instead of len(result).
    • Also updated the docstring of get_number_of_intersections to clarify that it could now be applied to results from gca_gca_intersection too.
  • Removes scalarized versions of methods where non-scalarized versions now suffice. I.e., removed _accux_constlat_scalar (use _accux_constlat instead), and _snap_const_lat_endpoint_xy (use _snap_const_lat_endpoint instead). By using tuples instead of tiny numpy arrays, the "non-scalar" versions also avoid allocation costs, so there is no performance-related need to continue maintaining the scalarized versions.
  • Similarly, updated arcs.py, removing _on_minor_arc_xyz by moving its logic into on_minor_arc, since the scalarized version wasn't being used anywhere anymore (except in benchmarking suite, which has been updated appropriately to use on_minor_arc). Updated on_minor_arc docstring to clarify inputs don't need to be numpy arrays; tuples of length 3 also work just fine.
  • Improved various docstrings, updating docstrings where needed to explain new behavior, but also adding to existing docstrings to clarify meanings and expectations about inputs and outputs, and removing now-obsolete notes about preferring scalarized versions.

EDIT: discussions below revealed the need to incorporate the following changes, too:

  • update the geometry_samebody.py and geometry_samebody_gcagca.py benchmarks files to keep the fp64 function performance in line with the optimizations to the corresponding functions from uxarray.
    • While doing that change, I also noticed and fixed: _fp64_try_gca_gca_intersection was using "and" instead of multiplying together 0s and 1s, so fp64 vs accux wasn't actually the only difference between it and intersections.py's _try_gca_gca_intersection.

EDIT: minor expansions of scope (requested in comment below):

  • Swapped np.max to np.nanmax in geometry_samebody_gcagca.py:274, to match the const-lat sibling at geometry_samebody.py:296.
  • Swapped njit decorators for all _fp64 methods in geometry_samebody and geometry_samebody_gcagca benchmarks to exactly match decorators from intersections.py. This fix may help to resolve Investigate Python thread-scaling of AccuX vs FP64 kernels #1587(?)

PR Checklist

General

  • An issue is created and linked
  • Added appropriate labels (if your uxarray repo permissions allow it)
  • Filled out Overview and Expected Usage (if applicable) sections

Testing & Benchmarking

  • There is adequate test coverage of changes from this PR (add new tests if needed)
  • If this PR could affect performance, ran ASV benchmarks and confirmed they show expected behavior (add a new benchmark if necessary)

Documentation and Examples

  • Docstrings updated with any function changes, and included in all new functions
  • User (public) functions added to docs/api.rst; internal (private) function names start with an underscore (_)
  • [N/A] If touched any notebook files, cleared the output of all cells before committing
  • [N/A] If added new notebook files, put into appropriate directories and referenced in appropriate files

AI Disclosure

AI Usage: GitHub Copilot's inline code suggestions, plus some Claude for numba debugging questions

  • I have tested and take responsibility for all AI-generated content in my PR.

undo many changes from #1688 because tuples are easier to read than scalars, and maintain all performance improvements. (There was some intent there to refactor back to vectors, but it got merged before that happened, with the intent to refactor being pushed as something to be handled later.)

Also optimizes to avoid creating numpy array of shape (2,3) for every gca_gca_intersection() call.

And, updates gca_gca_intersection() output to always be a tuple of 2 length-3 tuples, filling with NaNs, as promised in the docstring, instead of a numpy array with shape depending on number of intersections.
and other relevant intersections methods. Removes scalarized versions of related methods: _accux_constlat_scalar, _snap_const_lat_endpoint_xy, _on_minor_arc_xyz,  (if you really want to provide a bunch of scalars, just convert to tuples at call site; using tuples has the same performance as a scalarized implementation but it means there is no longer any need to maintain two versions of each method.)
@Sevans711 Sevans711 self-assigned this Sep 1, 2026
@Sevans711 Sevans711 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [8d2ac40] After [94050d6] Ratio Benchmark (Parameter)
- 1.55±0.04μs 706±30ns 0.45 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
- 1.14±0.04μs 752±50ns 0.66 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
- 1.94±0.03μs 857±10ns 0.44 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
* failed 831±20ns n/a geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
* failed 932±30ns n/a geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
* failed 1.12±0.02μs n/a geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
* failed 3.11±0.03ms n/a geometry_samebody.SameBodyConstLat.time_accux_dispatch
* failed 1.19±0.01ms n/a geometry_samebody.SameBodyConstLat.time_accux_kernel
* failed 2.33±0.02ms n/a geometry_samebody.SameBodyConstLat.time_fp64_dispatch
* failed 156±9μs n/a geometry_samebody.SameBodyConstLat.time_fp64_kernel
* failed 30.3±0.09ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
* failed 6.43±0.02ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
* failed 23.9±0.2ms n/a geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
* failed 895±1μs n/a geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel

Benchmarks that have stayed the same:

Change Before [8d2ac40] After [94050d6] Ratio Benchmark (Parameter)
5.96±0.1ms 6.03±0.04ms 1.01 bench_connectivity.Connectivity.time_edge_face('120km')
2.14±0.06ms 2.09±0.03ms 0.98 bench_connectivity.Connectivity.time_edge_face('480km')
4.76±0.05ms 4.94±0.04ms 1.04 bench_connectivity.Connectivity.time_edge_node('120km')
1.63±0.05ms 1.65±0.02ms 1.01 bench_connectivity.Connectivity.time_edge_node('480km')
4.75±0.2ms 4.93±0.04ms 1.04 bench_connectivity.Connectivity.time_face_edge('120km')
1.59±0.04ms 1.65±0.01ms 1.04 bench_connectivity.Connectivity.time_face_edge('480km')
6.90±0.2ms 6.99±0.06ms 1.01 bench_connectivity.Connectivity.time_face_face('120km')
2.46±0.04ms 2.51±0.03ms 1.02 bench_connectivity.Connectivity.time_face_face('480km')
73.1±4μs 71.4±5μs 0.98 bench_connectivity.Connectivity.time_face_node('120km')
67.4±4μs 66.2±2μs 0.98 bench_connectivity.Connectivity.time_face_node('480km')
487±10μs 496±20μs 1.02 bench_connectivity.Connectivity.time_n_nodes_per_face('120km')
426±10μs 428±20μs 1.00 bench_connectivity.Connectivity.time_n_nodes_per_face('480km')
6.18±0.09ms 6.33±0.04ms 1.02 bench_connectivity.Connectivity.time_node_edge('120km')
2.07±0.02ms 2.12±0.05ms 1.03 bench_connectivity.Connectivity.time_node_edge('480km')
82.0±2ms 88.0±2ms 1.07 bench_connectivity.Connectivity.time_node_face('120km')
5.06±0.04ms 5.22±0.07ms 1.03 bench_connectivity.Connectivity.time_node_face('480km')
8.96±0.8ms 9.75±0.1ms 1.09 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
3.07±0.3ms 3.22±0.1ms 1.05 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
10.3±10s 10.9±10ms ~0.00 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.18±0.03ms 2.15±0.05ms 0.99 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
57.3k 57.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
12.3k 12.3k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
123k 123k 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
128 128 1.00 face_bounds.FaceBounds.track_nbytes_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.27M 1.27M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
50.1k 50.1k 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
1.48M 1.48M 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
712 712 1.00 face_bounds.FaceBounds.track_nbytes_grid_with_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.96M 1.99M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.97M 2M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
2.13M 2.16M 1.01 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
38.3k 38.3k 1.00 face_bounds.FaceBounds.track_peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
336M 339M 1.01 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
368M 371M 1.01 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
338M 338M 1.00 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
337M 338M 1.00 face_bounds.FaceBoundsColdStartRss.peakmem_open_and_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.25±0.07μs 1.21±0.05μs 0.97 geometry_kernels.AccucrossKernels.time_accucross
2.66±0.02μs 2.75±0.03μs 1.04 geometry_kernels.AccucrossKernels.time_accucross_pair
441±20ns 446±20ns 1.01 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
441±20ns 441±20ns 1.00 geometry_kernels.EFTPrimitives.time_diff_of_products
401±20ns 406±20ns 1.01 geometry_kernels.EFTPrimitives.time_two_prod
366±20ns 376±20ns 1.03 geometry_kernels.EFTPrimitives.time_two_sum
52.5±0.5μs 53.5±0.8μs 1.02 geometry_kernels.OrientPredicates.time_on_minor_arc
1.17±0.07μs 1.11±0.03μs 0.94 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
873±10ms 907±8ms 1.04 import.Imports.timeraw_import_uxarray
293M 293M 1.00 import.Imports.track_peakmem_import_uxarray
2.74±0.05ms 2.82±0.06ms 1.03 mpas_ocean.CheckNorm.time_check_norm('120km')
2.24±0.03ms 2.28±0.1ms 1.01 mpas_ocean.CheckNorm.time_check_norm('480km')
1.28±0.04ms 1.27±0.03ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
598±10μs 604±10μs 1.01 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
742±20μs 770±8μs 1.04 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
666±10μs 670±10μs 1.01 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.49±0.08ms 5.52±0.09ms 1.01 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.96±0.05ms 4.09±0.03ms 1.03 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
100±0.6ms 104±0.7ms 1.04 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
10.9±0.5ms 11.1±0.4ms 1.02 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.2±0.04ms 18.4±0.2ms 1.01 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.05±0.01ms 1.08±0.02ms 1.02 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.03ms 10.7±0.05ms 1.01 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
754±20μs 742±10μs 0.98 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
595±4ms 602±5ms 1.01 mpas_ocean.CrossSections.time_const_lat('120km', 1)
302±4ms 306±6ms 1.01 mpas_ocean.CrossSections.time_const_lat('120km', 2)
157±1ms 162±4ms 1.03 mpas_ocean.CrossSections.time_const_lat('120km', 4)
538±6ms 545±5ms 1.01 mpas_ocean.CrossSections.time_const_lat('480km', 1)
274±2ms 271±1ms 0.99 mpas_ocean.CrossSections.time_const_lat('480km', 2)
138±2ms 139±1ms 1.00 mpas_ocean.CrossSections.time_const_lat('480km', 4)
357M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 1)
357M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 2)
356M 357M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('120km', 4)
339M 340M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 1)
340M 340M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 2)
340M 340M 1.00 mpas_ocean.CrossSectionsPeakMem.peakmem_const_lat('480km', 4)
25.8±1ms 27.3±0.8ms 1.06 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
3.41±0.6ms 4.13±0.1ms ~1.21 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
14.9±0.5ms 15.4±0.5ms 1.03 mpas_ocean.FaceAreas.time_face_areas('120km')
4.79±0.3ms 4.81±0.09ms 1.00 mpas_ocean.FaceAreas.time_face_areas('480km')
229k 229k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('120km')
14.3k 14.3k 1.00 mpas_ocean.FaceAreas.track_nbytes_face_areas('480km')
2.12M 2.12M 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('120km')
729k 729k 1.00 mpas_ocean.FaceAreas.track_peakmem_face_areas('480km')
938±4ms 942±8ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
52.5±0.7ms 55.1±2ms 1.05 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
85.0±0.6ms 84.4±0.6ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.86±0.1ms 6.25±0.1ms 1.07 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
15.1±0.3ms 15.5±0.1ms 1.03 mpas_ocean.Gradient.time_gradient('120km')
2.28±0.1ms 2.33±0.04ms 1.02 mpas_ocean.Gradient.time_gradient('480km')
457k 457k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('120km')
28.7k 28.7k 1.00 mpas_ocean.Gradient.track_nbytes_gradient('480km')
3.43M 3.43M 1.00 mpas_ocean.Gradient.track_peakmem_gradient('120km')
218k 218k 1.00 mpas_ocean.Gradient.track_peakmem_gradient('480km')
352M 352M 1.00 mpas_ocean.GradientColdStartRss.peakmem_gradient('120km')
331M 331M 1.00 mpas_ocean.GradientColdStartRss.peakmem_gradient('480km')
386±10μs 409±10μs 1.06 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
206±10μs 225±10μs 1.09 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
584±10μs 590±20μs 1.01 mpas_ocean.Integrate.time_integrate('120km')
521±20μs 523±10μs 1.00 mpas_ocean.Integrate.time_integrate('480km')
18.4M 18.4M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('120km')
1.2M 1.2M 1.00 mpas_ocean.Integrate.track_nbytes_integrate('480km')
185±1ms 186±0.9ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
184±1ms 184±2ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
189±1ms 189±0.7ms 1.00 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
14.1±0.1ms 14.7±0.2ms 1.04 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
14.1±0.1ms 14.7±0.4ms 1.04 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
14.1±0.1ms 14.9±0.2ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
241±0.8ms 257±10ms 1.07 mpas_ocean.NeighborhoodBuild.time_build('120km', 1.0)
1.29±0.02s 1.28±0s 1.00 mpas_ocean.NeighborhoodBuild.time_build('120km', 15.0)
511±4ms 505±4ms 0.99 mpas_ocean.NeighborhoodBuild.time_build('120km', 5.0)
13.3±0.3ms 13.6±0.2ms 1.03 mpas_ocean.NeighborhoodBuild.time_build('480km', 1.0)
25.1±0.1ms 25.3±0.2ms 1.01 mpas_ocean.NeighborhoodBuild.time_build('480km', 15.0)
16.6±0.3ms 16.5±0.4ms 1.00 mpas_ocean.NeighborhoodBuild.time_build('480km', 5.0)
237±2ms 235±1ms 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 1.0)
1.28±0.01s 1.26±0.01s 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 15.0)
496±5ms 492±3ms 0.99 mpas_ocean.NeighborhoodBuild.time_query_radius('120km', 5.0)
12.9±0.2ms 12.8±0.2ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 1.0)
24.5±0.3ms 25.0±0.3ms 1.02 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 15.0)
16.0±0.1ms 16.0±0.06ms 1.00 mpas_ocean.NeighborhoodBuild.time_query_radius('480km', 5.0)
1.19 1.19 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 1.0)
612.76 612.76 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 15.0)
74.17 74.17 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('120km', 5.0)
1.0 1.0 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 1.0)
37.29 37.29 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 15.0)
6.57 6.57 1.00 mpas_ocean.NeighborhoodBuild.track_mean_neighbors('480km', 5.0)
728k 728k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 1.0)
141M 141M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 15.0)
17.4M 17.4M 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('120km', 5.0)
43k 43k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 1.0)
563k 563k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 15.0)
123k 123k 1.00 mpas_ocean.NeighborhoodBuild.track_nbytes_neighbors('480km', 5.0)
5.72M 5.72M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 1.0)
145M 145M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 15.0)
21.5M 21.5M 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('120km', 5.0)
362k 362k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 1.0)
825k 824k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 15.0)
384k 384k 1.00 mpas_ocean.NeighborhoodBuild.track_peakmem_build('480km', 5.0)
45.6±0.9ms 46.9±0.7ms 1.03 mpas_ocean.NeighborhoodDask.time_mean('120km', 'grid_chunks')
22.7±0.2ms 23.0±0.2ms 1.01 mpas_ocean.NeighborhoodDask.time_mean('120km', 'numpy')
43.2±1ms 42.8±1ms 0.99 mpas_ocean.NeighborhoodDask.time_mean('120km', 'time_chunks')
12.0±0.5ms 12.5±0.2ms 1.04 mpas_ocean.NeighborhoodDask.time_mean('480km', 'grid_chunks')
803±90μs 853±20μs 1.06 mpas_ocean.NeighborhoodDask.time_mean('480km', 'numpy')
8.95±0.1ms 8.91±0.2ms 1.00 mpas_ocean.NeighborhoodDask.time_mean('480km', 'time_chunks')
5.84M 5.83M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'grid_chunks')
2.75M 2.75M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'numpy')
5.68M 5.67M 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('120km', 'time_chunks')
646k 656k 1.02 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'grid_chunks')
177k 177k 1.00 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'numpy')
532k 529k 0.99 mpas_ocean.NeighborhoodDask.track_peakmem_mean('480km', 'time_chunks')
12.7±0s 12.6±0s 0.99 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'mean')
13.2±0s 13.3±0s 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('120km', 'median')
228±0.7ms 231±2ms 1.01 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'mean')
237±0.5ms 238±2ms 1.00 mpas_ocean.NeighborhoodReduce.time_dataset_reduce('480km', 'median')
1.32±0s 1.33±0.01s 1.00 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'mean')
1.52±0.01s 1.54±0.02s 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('120km', 'median')
25.9±0.2ms 26.2±0.4ms 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'mean')
27.3±0.2ms 27.6±0.5ms 1.01 mpas_ocean.NeighborhoodReduce.time_neighborhood_reduce('480km', 'median')
41.1±0.9ms 43.3±0.4ms 1.05 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'mean')
236±1ms 240±1ms 1.02 mpas_ocean.NeighborhoodReduce.time_reduce('120km', 'median')
639±70μs 700±50μs 1.09 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'mean')
2.10±0.03ms 2.12±0.04ms 1.01 mpas_ocean.NeighborhoodReduce.time_reduce('480km', 'median')
239k 239k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'mean')
245k 245k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('120km', 'median')
19.4k 19.4k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'mean')
19.9k 19.9k 1.00 mpas_ocean.NeighborhoodReduce.track_peakmem_reduce('480km', 'median')
400±20μs 408±10μs 1.02 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
375±8μs 403±10μs 1.08 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
387±10μs 377±10μs 0.98 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
367±20μs 358±10μs 0.97 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
236±1ms 235±0.3ms 1.00 mpas_ocean.RemapDownsample.time_bilinear_remapping
293±1ms 292±3ms 1.00 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
15.8±0.1ms 16.7±0.4ms 1.06 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.35±0.01s 1.35±0.03s 1.00 mpas_ocean.RemapUpsample.time_bilinear_remapping
38.4±0.2ms 37.7±1ms 0.98 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
13.0±0.5ms 13.1±0.6ms 1.00 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
9.92±0.1ms 9.90±0.3ms 1.00 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.56±0.09ms 5.55±0.1ms 1.00 mpas_ocean.ZonalAverage.time_zonal_average('480km')
358M 359M 1.00 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('120km')
342M 342M 1.00 mpas_ocean.ZonalAveragePeakMem.peakmem_zonal_average('480km')
6.80±0.07ms 6.95±0.1ms 1.02 quad_hexagon.QuadHexagon.time_open_dataset
5.90±0.09ms 5.82±0.3ms 0.99 quad_hexagon.QuadHexagon.time_open_grid
408 408 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_dataset
392 392 1.00 quad_hexagon.QuadHexagon.track_nbytes_open_grid
73.8k 73.8k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_dataset
73k 73k 1.00 quad_hexagon.QuadHexagon.track_peakmem_open_grid

@Sevans711
Sevans711 marked this pull request as ready for review September 2, 2026 18:37
@Sevans711

Copy link
Copy Markdown
Collaborator Author

Marking as ready for review because tests passed and ASV benchmarks look good. Pinging @cmdupuis3 regarding the gca_gca non-constant-latitude benchmarks, since they are failing on main so it is hard to compare. Do you think another intersections-related benchmark needs to be added as part of this PR or is it okay to move forward as-is?

@erogluorhan
erogluorhan removed the request for review from rajeeja September 2, 2026 20:44
@cmdupuis3

Copy link
Copy Markdown
Collaborator

@Sevans711 We should be able to compare it to main commits prior to #1688, the latest of which is 7eff90d. I can do that and report the results here if you'd like.

@Sevans711

Copy link
Copy Markdown
Collaborator Author

@cmdupuis3 Ah, yes if you are able to do that comparison and share results here that would be great!

Comment thread uxarray/grid/intersections.py Outdated

@dylannelson dylannelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking around here, one small comment

@dylannelson dylannelson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything I ran came through clean. One minor line was fixed. I didn't see any blockers. Hoping tests done with Chris turn out well. Looks good on my end! Thanks!

@cmdupuis3

cmdupuis3 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

The timings are pretty small, so some wide variability is expected, but this is what I see:

Change Before [7eff90d] <v2026.08.1~2> After [4e34008] <sevans/tinynumpynumba-optimize-intersections.py> Ratio Benchmark (Parameter)
- 667±0.5ns 417±0ns 0.63 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
- 1.08±0.06μs 500±20ns 0.46 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
- 833±40ns 375±0ns 0.45 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel

@cmdupuis3 cmdupuis3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All tests pass, and the benchmarks show improvements. I had Claude scan for accuracy divergences and none were found, although LLMs aren't great for that. Considering that this is a sort-of reversion of #1688, that should be expected.

A few benchmarks still need to be migrated though:

  • geometry_samebody.py:270 — import renamed _accux_constlat_scalar → _accux_constlat, but the px, py, nxo, nyo = ... 4-way unpack was kept; the new kernel returns two 3-tuples. TypingError, which takes down the asv class SameBodyConstLat.

  • geometry_samebody.py:292 — res[0, 0] 2D-indexes the now-tuple return of gca_const_lat_intersection.

  • geometry_samebody.py:345 — same 2D indexing in main()'s accuracy cross-check.

Comment thread benchmarks/geometry_samebody_gcagca.py Outdated
@Sevans711

Copy link
Copy Markdown
Collaborator Author

A few benchmarks still need to be migrated though: …

@cmdupuis3 Thank you for reviewing and pointing these out! I now see that they were originally reported as "failed" in the ASV benchmarking suite results comment above. I believe I have fixed all of these with the latest commit.

@Sevans711
Sevans711 requested a review from cmdupuis3 September 4, 2026 20:48

@cmdupuis3 cmdupuis3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues:

  1. geometry_samebody_gcagca.py:274 uses np.max; the const-lat sibling correctly uses np.nanmax at geometry_samebody.py:296. Since a 1-intersection result is now (point, (nan,nan,nan)), np.max returns NaN — and max(0.0, nan) returns 0.0, because NaN loses every comparison.

  2. geometry_samebody_gcagca.py:61 _fp64_try_gca_gca_intersection is plain @njit(cache=True), but production _try_gca_gca_intersection is @njit(cache=True, inline="always", ...). This means the FP64 version will show different behavior in benchmarks, so the FP64 version should have inline="always" as well.

@Sevans711

Copy link
Copy Markdown
Collaborator Author

Two issues:

  1. geometry_samebody_gcagca.py:274 uses np.max; the const-lat sibling correctly uses np.nanmax at geometry_samebody.py:296. …

  2. geometry_samebody_gcagca.py:61 _fp64_try_gca_gca_intersection is plain @njit(cache=True), but production _try_gca_gca_intersection is @njit(cache=True, inline="always", ...)…

Note that both of these are currently issues on main, not being introduced by this PR. I do think it is reasonable to solve them here though. I've updated the original post of this PR to clarify that the scope has expanded to include these fixes.

Both should be fixed now! Note that I also updated the decorators for the other _fp64 benchmarks too, to exactly match their production counterparts (many were missing error_model="numpy"; not sure if this matters, but I think it makes sense to make them exactly the same.)

@Sevans711
Sevans711 requested a review from cmdupuis3 September 8, 2026 14:56
@Sevans711
Sevans711 merged commit 38b33d8 into main Sep 9, 2026
16 checks passed
@Sevans711
Sevans711 deleted the sevans/tinynumpynumba-optimize-intersections.py branch September 9, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-benchmark Run ASV benchmark workflow scalability Related to scalability & performance efforts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid tiny numpy arrays in numba: optimize intersections.py Investigate Python thread-scaling of AccuX vs FP64 kernels

3 participants